import {
  arrayBuffer2base64String,
  base64String2arrayBuffer,
  base64String2string,
  blob2arrayBuffer,
  dirname,
  requestBlob,
  requestString,
  sliceArrayBuffer
} from "@loaders/commons";
import { parseGLBSync } from "./parses/parse-glb";
import { getComponentSize, accessorSizes } from "./util";

export class CacheData {
  constructor() {}
}

export class GLTFData {
  private _cacheBinChunks: Record<string, any>[] = [];
  private _cacheBuffers: Record<string, any> = {};
  private _cachePrograms: Record<string, any> = {};
  private _cacheTechniques: Record<string, any> = {};
  private _cacheTextures: Record<string, any> = {};
  private _data: Record<string, any> = {};
  private _version: string;
  private _dirname: string;
  private _extensionsKey: string;
  constructor(public url: string, public parsedCallback: Function = () => {}) {
    this._dirname = dirname(url);
    const request = (url.indexOf("glb") > -1 ? requestBlob : requestString) as Function;
    request(url).then((data: string | Blob) => {
      if (typeof data === "string") {
        this._data = JSON.parse(data);
        this._version = this.getVersion();
        this._parse();
      } else if (data instanceof Blob) {
        var fr = new FileReader(); //FileReader可以读取Blob内容
        fr.readAsArrayBuffer(data); //二进制转换成ArrayBuffer
        fr.onload = (e) => {
          // 转换完成后，调用onload方法
          const arrayBuffer = fr.result as ArrayBuffer; //result 转换的结果
          const parsedData: Record<string, any> = {};
          parseGLBSync(parsedData, arrayBuffer);
          this._data = parsedData.json;
          this._version = this.getVersion();
          this._cacheBinChunks = parsedData.binChunks;
          this._parse();
        };
      }
    });
  }

  getVersion() {
    let ret = "asset.version".split(".").reduce((ret, item) => {
      if (ret == null || ret[item] == null) return null;
      return ret[item];
    }, this._data);

    return ret || "1.0";
  }

  async _parse() {
    const extensionsUsed = this._data.extensionsUsed;
    this._extensionsKey = (extensionsUsed && extensionsUsed[0]) || "";
    await this._initBuffers();
    await this._initTextures();
    await this._initPrograms();
    await this._initTechniques();
    await this._initReJSON();
  }

  isV1() {
    return this._version === "1.0";
  }

  isV2() {
    return this._version === "2.0";
  }

  // accessors, animations, buffers, bufferViews, cameras, images, materials, meshes, nodes, programs, samplers, scenes, shaders, skins, techniques, and textures
  _topLevel(name: string) {
    return this._data[name];
  }

  async _initBuffers() {
    if (!!this._data.buffers === false) return;
    const buffers = this._data.buffers;
    const keys = Object.keys(buffers);
    let allPromise = [];
    keys.forEach((key) => {
      const buffer = buffers[key];
      allPromise.push(
        new Promise((resolve, reject) => {
          if (/;base64,/.test(buffer.uri)) {
            resolve(base64String2arrayBuffer(buffer.uri));
          } else if (/\.glb$/.test(this.url)) {
            // glb
            const arraybuffer = sliceArrayBuffer(
              this._cacheBinChunks[0].arrayBuffer,
              this._cacheBinChunks[0].byteOffset,
              undefined
            );
            resolve(arraybuffer);
            // debugger
          } else if (/\.bin$/.test(buffer.uri)) {
            //外
            requestBlob(`${this._dirname}/${buffer.uri}`).then((blob) => {
              blob2arrayBuffer(blob).then((arraybuffer) => {
                resolve(arraybuffer);
              });
            });
          }
        })
      );
    });
    return new Promise((resolve, reject) => {
      Promise.all(allPromise).then((datas) => {
        keys.forEach((key, ind) => {
          this._cacheBuffers[key] = datas[ind];
        });
        console.log(this._cacheBuffers);
        resolve(datas);
      });
    });
  }

  async _initTechniques() {
    if (!!this._data.techniques === false) return;
    Object.keys(this._data.techniques).forEach(async (t) => {
      const technique = this._data.techniques[t];

      this._cacheTechniques[t] = {
        ...technique,
        program: this._cachePrograms[technique.program]
      };
    });
  }

  async _initTextures() {
    if (!!this._data.textures === false) return;
    Object.keys(this._data.textures).forEach(async (t) => {
      const texture = this._data.textures[t];
      let image = this.getByIdOrName("images", texture.source);
      // image.uri;
      let source;
      if (image.extensions) {
        const info = image.extensions[this._extensionsKey];
        const bufferviewName = info.bufferView;
        const bufferview = this.getByIdOrName("bufferViews", bufferviewName);
        const fsbuffer = this._cacheBuffers[bufferview.buffer];
        const subBuffer = sliceArrayBuffer(fsbuffer, bufferview.byteOffset, bufferview.byteLength);
        const s = arrayBuffer2base64String(subBuffer);
        source = `data:${info.mimeType};base64,${s}`;
      } else {
        source = image.uri;
      }

      this._cacheTextures[t] = {
        ...texture,
        source,
        sampler: this.getByIdOrName("samplers", texture.sampler)
      };
    });
  }

  async _initPrograms() {
    if (!!this._data.programs === false) return;
    let promises = [];
    Object.keys(this._data.programs).forEach((p) => {
      const program = this._data.programs[p];
      const fs = this.getByIdOrName("shaders", program.fragmentShader);
      const vs = this.getByIdOrName("shaders", program.vertexShader);
      let fragmentShader = "";
      let vertexShader = "";
      if (/;base64,/.test(fs.uri)) {
        fragmentShader = base64String2string(`${this._dirname}/${fs.uri}`);
        vertexShader = base64String2string(`${this._dirname}/${vs.uri}`);

        this._cachePrograms[p] = {
          ...program,
          fragmentShader,
          vertexShader
        };
      } else if (/\.glb$/.test(this.url)) {
        // glb
        const fsextensions = fs.extensions;
        const vsextensions = vs.extensions;
        const fsbufferViewFlag = fsextensions[this._extensionsKey].bufferView;
        const vsbufferViewFlag = vsextensions[this._extensionsKey].bufferView;
        const fsbufferview = this.getByIdOrName("bufferViews", fsbufferViewFlag);
        const vsbufferview = this.getByIdOrName("bufferViews", vsbufferViewFlag);
        const fsbuffer = this._cacheBuffers[fsbufferview.buffer];
        const vsbuffer = this._cacheBuffers[vsbufferview.buffer];
        const r = new Uint8Array(fsbuffer, fsbufferview.byteOffset, fsbufferview.byteLength);
        const y = new Uint8Array(vsbuffer, vsbufferview.byteOffset, vsbufferview.byteLength);
        const textDecoder = new TextDecoder("utf8");
        fragmentShader = textDecoder.decode(r);
        vertexShader = textDecoder.decode(y);

        this._cachePrograms[p] = {
          ...program,
          fragmentShader,
          vertexShader
        };
      } else if (/\.glsl/.test(fs.uri)) {
        this._cachePrograms[p] = {
          ...program,
          fragmentShader: null,
          vertexShader: null
        };
        promises.push(
          new Promise((res) => {
            requestString(`${this._dirname}/${fs.uri}`).then((data) => {
              this._cachePrograms[p].fragmentShader = data;
              res(0);
            });
          })
        );

        promises.push(
          new Promise((res) => {
            requestString(`${this._dirname}/${vs.uri}`).then((data) => {
              this._cachePrograms[p].vertexShader = data;
              res(0);
            });
          })
        );
      }
    });

    return Promise.all(promises).then(() => {
      console.log(this._cachePrograms);
    });
  }

  async _initReJSON() {
    let sceneName = this._data.scene;
    const nodes = this.getByIdOrName("scenes", sceneName).nodes;
    const cacheData: Record<string, any> = [];

    for (let i = 0, len = nodes.length; i < len; i++) {
      const sceneNodeIdOrName = nodes[i];
      const sceneNode = this.getByIdOrName("nodes", sceneNodeIdOrName);
      const children = sceneNode.children || [];

      const cacheNode: Record<string, any> = {
        sceneIdOrName: sceneNodeIdOrName,
        children: []
      };

      for (let j = 0, len = children.length; j < len; j++) {
        const nodeIdOrName = children[j];
        const node = this.getByIdOrName("nodes", nodeIdOrName);
        if (node.meshes === undefined && node.mesh === undefined) {
          console.warn("为父结点");
          continue;
        }
        const meshes = node.meshes || [node.mesh];

        const cacheMesh: Record<string, any> = {
          nodeIdOrName: nodeIdOrName,
          children: []
        };
        for (let k = 0, len = meshes.length; k < len; k++) {
          const meshIdOrName = meshes[k];
          const mesh = this.getByIdOrName("meshes", meshIdOrName);
          //
          const r = {
            idOrName: meshIdOrName,
            meshes: {
              name: mesh.name,
              primitives: []
            }
          };
          for (const { attributes: attrs, indices, material, mode, aaa } of mesh.primitives) {
            const attributes = {};
            Object.keys(attrs).forEach((attr) => {
              const data = this._getDataFromBuffer(attrs[attr]);
              attributes[attr] = data;
            });

            let indData = null;
            if (indices !== undefined) {
              indData = this._getDataFromBuffer(indices);
            }

            let mtl = this.getByIdOrName("materials", material);
            let technique = this._cacheTechniques[mtl.technique] || {};
            r.meshes.primitives.push({
              mode: this.getMode(mode),
              material: {
                ...mtl,
                technique: technique
              },
              program: technique.program,
              indices: indData || [],
              attributes
            });
          }

          cacheMesh.children.push(r);
        }

        cacheNode.children.push(cacheMesh);
      }
      cacheData.push(cacheNode);
    }
    console.log(cacheData);
    const that = this;
    this.parsedCallback({
      version: that._version,
      programs: that._cachePrograms,
      techniques: that._cacheTechniques,
      textures: that._cacheTextures,
      nodes: cacheData,
      origin: that._data
    });
  }

  _getDataFromBuffer(attr: string | number) {
    var data;
    let accessor = this.getByIdOrName("accessors", attr);
    const bufferView = this.getByIdOrName("bufferViews", accessor.bufferView);
    const buffer = this._cacheBuffers[bufferView.buffer];
    const byteOffset = accessor.byteOffset + bufferView.byteOffset;
    const componentSize = getComponentSize(accessor.componentType);
    let componentCount = accessorSizes[accessor.type];
    if (bufferView.byteStride > 0) {
      componentCount = bufferView.byteStride / componentSize;
    }

    const arrayLength = accessor.count * componentCount;

    switch (accessor.componentType) {
      case WebGLRenderingContext.BYTE:
        data = new Int8Array(buffer, byteOffset, arrayLength);
        break;
      case WebGLRenderingContext.UNSIGNED_BYTE:
        data = new Uint8Array(buffer, byteOffset, arrayLength);
        break;
      case WebGLRenderingContext.SHORT:
        data = new Int16Array(buffer, byteOffset, arrayLength);
        break;
      case WebGLRenderingContext.UNSIGNED_SHORT:
        data = new Uint16Array(buffer, byteOffset, arrayLength);
        break;
      case WebGLRenderingContext.UNSIGNED_INT:
        data = new Uint32Array(buffer, byteOffset, arrayLength);
        break;
      case WebGLRenderingContext.FLOAT:
        data = new Float32Array(buffer, byteOffset, arrayLength);
        break;
    }
    return data;
  }

  getByIdOrName(topLevel: string, idOrName: string | number) {
    return this._data[topLevel][idOrName];
  }

  getBufferData(idOrName: string | number) {
    const accessor = this.getByIdOrName("accessors", idOrName);
  }

  getMode(mode: number) {
    const type = ["POINTS", "LINES", "LINE_LOOP", "LINE_STRIP", "TRIANGLES", "TRIANGLE_STRIP", "TRIANGLE_FAN"][mode];
    return WebGLRenderingContext[type];
  }
}
